package be.rivendale.geometry;

import be.rivendale.mathematics.*;
import org.apache.commons.lang.Validate;

/**
 * Defines a drawable view onto the virtual world. It is through a camera that the user 'sees' the world.
 * Different camera properties determine various viewing parameters, such as viewing angle, aspect ratio, etc...
 */
public class Camera {
    /**
     * The aspect ratio between vertical and horizontal dimensions of the view plane rectangle.
     */
    private static final double ASPECT_RATIO = 16.0 / 9.0;

    /**
     * The direction in which this camera points.
     */
    private static final Vector DIRECTION_VECTOR = new Triple(0, 0, 1);
    private static final Vector VERTICAL_VIEW_PLANE_VECTOR = new Triple(1, 0, 0);
    private static final Vector HORIZONTAL_VIEW_PLANE_VECTOR = new Triple(0, -1, 0);

    /**
     * Defines the minimum width and height resolution of the pixel raster to cast rays through.
     * <p>This is limited to {@code >= 2} because if the resolution is less then 2, it's not possible to cast rays evenly so
     * that it's minimum and maximum values are rays cast through the bounding points of the {@link #viewPlane}
     * rectangle.<br/>If the resolution is 2 or more, the first pixel's ray is cast through the left (horizontally)/top
     * (vertically) view port point, and the last one is cast through the right (horizontally)/bottom (vertically) view
     * port point.</p>
     */
    private static final int MINIMUM_RESOLUTION_TO_CAST_RAYS = 2;

    /**
     * The focus point of the camera.
     * This point is always behind the {@link #viewPlane}
     */
    private Point focusPoint;

    /**
     * The viewport rectangle through which the world is seen.
     * This rectangle always has point A as it's lower left corner, point B as it's upper left corner,
     * point C as it's lower right corner and point D as it's upper right corner.
     */
    private Rectangle viewPlane;

    public Camera(Point focusPoint, double focalLength, double verticalFilmSize) {
        Point centerOfViewPlane = focusPoint.add(DIRECTION_VECTOR.multiply(focalLength).asPoint());
        double horizontalFilmSize = verticalFilmSize * ASPECT_RATIO;

        Point bottomLeftCornerOfViewPlane = centerOfViewPlane.subtract(
                VERTICAL_VIEW_PLANE_VECTOR.multiply(verticalFilmSize / 2).asPoint()
        ).subtract(
                HORIZONTAL_VIEW_PLANE_VECTOR.multiply(horizontalFilmSize / 2).asPoint()
        );

        Point bottomRightCornerOfViewPlane = bottomLeftCornerOfViewPlane.add(HORIZONTAL_VIEW_PLANE_VECTOR.multiply(horizontalFilmSize).asPoint());
        Point topLeftCornerOfViewPlane = bottomLeftCornerOfViewPlane.add(VERTICAL_VIEW_PLANE_VECTOR.multiply(verticalFilmSize).asPoint());

        this.focusPoint = focusPoint;
        this.viewPlane = new Rectangle(bottomLeftCornerOfViewPlane, topLeftCornerOfViewPlane, bottomRightCornerOfViewPlane);
    }

    public Camera() {
        this.viewPlane = new Rectangle(new Triple(-4, 3, 0),
                new Triple(-4, -3, 0),
                new Triple(4, 3, 0));
        this.focusPoint = new Triple(0, 0, 5);
    }

    /**
     * Returns the {@link #focusPoint} if this camera.
     * @return The focuspoint of this camera.
     */
    public Point getFocusPoint() {
        return focusPoint;
    }

    /**
     * Returns the {@link #viewPlane} rectangle of this camera.
     * @return The viewport of this camera.
     */
    public Rectangle getViewPlane() {
        return viewPlane;
    }

    /**
     * Casts a {@link Ray} starting from the {@link #focusPoint} (which is the ray's origin).
     * The ray passes through a point on the {@link #viewPlane} and continues in that direction to infinity.
     * <p>The point where it passes through the viewport, is defined by the specified parameters. The width and height parameters
     * determine the resolution of the pixel raster to cast rays for. This resolution is required to allow this method to calculate the relative
     * position of the x and y points on the viewport. The x and y parameters define the pixel for which a ray is to be cast.</p>
     * <p><strong>Example:</strong><br/>
     * Using parameters <code>x=10, y=10, width=640, height=480</code>, the ray's secondary point is calculated as folows:<br/>
     * <ul><li>the width of the viewPort is divided by 640, which is the distance on the viewport between two rays.</li>
     * <li>This distance is multiplied by the value of x, so that we get the horizontal location of the 10th ray's secondary point.</li>
     * <li>The same process is performed to get the vertical location of of the point on the viewport.</li>
     * <li>These two locations determine the position on the viewport to pass the ray through.</li></ul>
     * These calculations are of course a simplified example, since the real calculations are in 3D.</p>
     * @param x The pixel raster's (2D) horizontal location of the pixel we are casting a ray for.
     * @param y The pixel raster's (2D) vertical location of the pixel we are casting a ray for.
     * @param width The with of the pixel raster. Required to calculate the relative position of X on the 3D viewport.
     * @param height The height of the pixel raster. Required to calculate the relative position of Y on the 3D viewport.
     * @return A ray which has this camera's focus point as it's origin, and a point on the viewport as it's secondary point.
     */
    public Ray castRay(int x, int y, int width, int height) {
        validateCastRayArguments(x, y, width, height);
        Point point = viewPlane.pointOnRectangle((double)y / (height - 1), (double)x / (width - 1));
        return new Ray(getFocusPoint(), point);
    }

    /**
     * Checks that the arguments suplied to cast a ray are valid.
     * @param x The horizontal pixel  to cast a ray for. Must be >= 0 < width
     * @param y The vertical pixel to cast a ray for. Must be >= 0 < height
     * @param width The number of pixels horizontally. Must be >= 2.
     * @param height The number of pixels vertically. Must be >= 2.
     * @throws IllegalArgumentException When at least one of the parameter is not valid.
     */
    private void validateCastRayArguments(int x, int y, int width, int height) throws IllegalArgumentException {
        Validate.isTrue(x >= 0 && y >= 0);
        Validate.isTrue(width >= MINIMUM_RESOLUTION_TO_CAST_RAYS && height >= MINIMUM_RESOLUTION_TO_CAST_RAYS);
        Validate.isTrue(x < width && y < height);
    }

    /**
     * Returns the top right corner of the viewport.
     * @return The top right corner.
     */
    Point getTopRightCorner() {
        return getViewPlane().getD();
    }

    /**
     * Returns the bottom right corner of the viewport.
     * @return The bottom right corner.
     */
    Point getBottomRightCorner() {
        return getViewPlane().getC();
    }

    /**
     * Returns the top left corner of the view port.
     * @return The top left corner.
     */
    Point getTopLeftCorner() {
        return getViewPlane().getB();
    }

    /**
     * Returns the bottom left corner of the view port.
     * @return the bottom left corner.
     */
    Point getBottomLeftCorner() {
        return getViewPlane().getA();
    }

	/**
	 * Calculates the focal length of this camera.
	 * The focal length is the distance between the {@link #getFocusPoint() focus point}  and the
	 * {@link Rectangle#center() center} of the {@link #getViewPlane() view plane}.
	 * <p>More info can be found <a href="http://en.wikipedia.org/wiki/Focal_length">here</a>.</p>
	 * @return The focal length of this camera.
	 */
	public double focalLength() {
		return Triple.vectorBetweenPoints(focusPoint, viewPlane.center()).length();
	}

	/**
	 * Calculates the horizontal angle of view for this camera.
	 * <p>The angle of view can be calculated horizontally, {@link #verticalViewAngle() vertically} and
	 * {@link #diagonalViewAngle() diagonally}.</p>
	 * <p>More info about the angle of view can be found <a href="http://en.wikipedia.org/wiki/Angle_of_view">here</a>.</p>
	 * @return The angle of view measured horizontally for this camera.
	 */
	public double horizontalViewAngle() {
		return 2 * Math.atan(horizontalViewPlaneSize() / (2 * focalLength()));
	}

	/**
	 * Calculates the horizontal dimensions of the camera's film, lens or {@link #getViewPlane() view plane}.
	 * @return The horizontal dimensions of the camera's film.
	 */
	public double horizontalViewPlaneSize() {
		return Triple.vectorBetweenPoints(getBottomLeftCorner(), getBottomRightCorner()).length();
	}

	/**
	 * Calculates the vertical angle of view for this camera.
	 * <p>The angle of view can be calculated {@link #horizontalViewAngle() horizontally}, vertically and
	 * {@link #diagonalViewAngle() diagonally}.</p>
	 * <p>More info about the angle of view can be found <a href="http://en.wikipedia.org/wiki/Angle_of_view">here</a>.</p>
	 * @return The angle of view measured vertically for this camera.
	 */
	public double verticalViewAngle() {
		return 2 * Math.atan(verticalViewPlaneSize() / (2 * focalLength()));
	}

	/**
	 * Calculates the vertical dimensions of the camera's film, lens or {@link #getViewPlane() view plane}.
	 * @return The vertical dimensions of the camera's film.
	 */
	public double verticalViewPlaneSize() {
		return Triple.vectorBetweenPoints(getBottomLeftCorner(), getTopLeftCorner()).length();
	}

	/**
	 * Calculates the diagonal angle of view for this camera.
	 * <p>The angle of view can be calculated {@link #horizontalViewAngle() horizontally}, {@link #verticalViewAngle()
	 * vertically} and diagonally.</p>
	 * <p>More info about the angle of view can be found <a href="http://en.wikipedia.org/wiki/Angle_of_view">here</a>.</p>
	 * @return The angle of view measured diagonally for this camera.
	 */
	public double diagonalViewAngle() {
		return 2 * Math.atan(diagonalViewPlaneSize() / (2 * focalLength()));
	}

	/**
	 * Calculates the diagonal dimensions of the camera's film, lens, or {@link #getViewPlane() view plane}.
	 * @return The diagonal dimensions of the camera's film.
	 */
	public double diagonalViewPlaneSize() {
		return Triple.vectorBetweenPoints(getBottomLeftCorner(), getTopRightCorner()).length();
	}

    public double aspectRatio() {
        return horizontalViewPlaneSize() / verticalViewPlaneSize();
    }
}
